/*
  Copyright (c) 2011, Mikheev Rostislav
  Copyright (c) 2011, FreshFarsh team

  Steel Engine source code is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation; either version 2
  of the License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/
/**
  @author Mikheev Rostislav <hacenator@gmail.com>
  @date 23.03.2011
  @brief Image loader source file
*/
#include "image.h"

#include <memory>
#include <time.h>

namespace _STEEL_NAMESPACE {

/*
 *	Basic Image
 */
Image::Image()
  : _width(0),
  _height(0),
  _channels(0),
  _dataSize(0),
  _format(0),
  _mipmaps_count(0),
  _loaded(false),
  _pixels(NULL)
{
  // pass
}

Image::~Image() {
  delete [] _pixels;
};

/**********************************************************************************************
	TGA Image Class
**********************************************************************************************/
TGAImage::TGAImage(IOFile & data) {
  TGAHeader header;

	_name = data.filename();

	data.Seek(0, SEEK_SET);
	if (data.Read(&header.data, sizeof(header)) != 18)
    throw RuntimeError() << "TGA file '" << _name.c_str() << "' header error.";

	// Image size
	_width  = header.width;
	_height = header.height;
	_channels = (header.bits / 8);

	if (_width <= 0 || _height  <= 0  || (_channels != 3 && _channels != 4)) {
    _width = 0;
    _height = 0;
    _channels = 0;

    throw RuntimeError() << "Wrong TGA file '" << _name.c_str() << "', only 24 and 32 not RLE commpressed image supported.";
	}

	_dataSize = _width * _height * _channels;

	// bgra ext
	_format = ((_channels == 3) ? GL_BGR_EXT : GL_BGRA_EXT);
	_pixels = new GLubyte[_dataSize];
	if (data.Read(_pixels, _dataSize) != _dataSize) {
    _width = 0;
    _height = 0;
    _channels = 0;
    delete _pixels;

    throw RuntimeError() << "Can not read TGA file '" << _name.c_str() << "'. Unexpected end of file";
	}

  // Image sucessfully loaded
	_loaded = true;
}

TGAImage::~TGAImage(void) {
	delete [] _pixels;
}

/**********************************************************************************************
	JPG Image Class
**********************************************************************************************/
JPGImage::JPGImage(IOFile & data) {
  jpeg_decompress_struct cinfo;
	jpeg_error_mgr jerr;
	jpeg_source_mgr jsrc;
	JSAMPROW line;

  _name = data.filename();

	try {
		// Configure error manager
		jerr.error_exit = ErrorExit;
		jerr.output_message = OutputMessage;
		cinfo.err = jpeg_std_error(&jerr);

		// Create and configure decompress object
		jpeg_create_decompress(&cinfo);
		//cinfo.src = &jsrc;

    jpeg_stdio_src(&cinfo, data.file());

		// Read file's header and prepare for decompression
		jpeg_read_header(&cinfo, true);
    //cinfo.do_fancy_upsampling = FALSE; // fast loading

		// Initialize image's member variables
		_width = cinfo.image_width;
		_height = cinfo.image_height;
		_channels = cinfo.num_components;

		_format = ((_channels == 1) ? GL_LUMINANCE : GL_RGB);
		_dataSize = _width * _height * _channels;
		_pixels = new GLubyte[_dataSize];

		// Decompress jpg file
		jpeg_start_decompress (&cinfo);

		// Read scanlines
		for (int i = 0; i < _height; ++i) {
			line = (_pixels + ((_height - (i + 1)) * _width * _channels));
			jpeg_read_scanlines(&cinfo, &line, 1);
		}

		// Finish decompression and release memory
		jpeg_finish_decompress (&cinfo);
		jpeg_destroy_decompress (&cinfo);

		// image sucsesfully loaded
		_loaded = true;

  } catch (...) {
		delete [] _pixels;
		jpeg_destroy_decompress (&cinfo);

    _width = 0;
    _height = 0;
    _channels = 0;
    _loaded = false;

    throw RuntimeError() << "Jpeg file '" << _name.c_str() << "' loading error.";
	}
}

JPGImage::~JPGImage() {
  delete [] _pixels;
}

//
void JPGImage::InitSource(j_decompress_ptr cinfo) {
	//
}

bool JPGImage::FillInputBuffer(j_decompress_ptr cinfo) {
  JOCTET eoi_buffer[2] = { 0xFF, JPEG_EOI };
  struct jpeg_source_mgr *jsrc = cinfo->src;

  // Create a fake EOI marker
  jsrc->next_input_byte = eoi_buffer;
  jsrc->bytes_in_buffer = 2;

  return true;
}

void JPGImage::SkipInputData(j_decompress_ptr cinfo, long num_bytes) {
  struct jpeg_source_mgr *jsrc = cinfo->src;

  if (num_bytes > 0) {
    while (num_bytes > static_cast<long>(jsrc->bytes_in_buffer)) {
      num_bytes -= static_cast<long>(jsrc->bytes_in_buffer);
      FillInputBuffer(cinfo);
    }

    jsrc->next_input_byte += num_bytes;
    jsrc->bytes_in_buffer -= num_bytes;
  }
}

void JPGImage::TermSource(j_decompress_ptr cinfo) {
//
}

void JPGImage::ErrorExit(j_common_ptr cinfo) {
	jpeg_error_mgr * jerr = cinfo->err;
}

void JPGImage::OutputMessage(j_common_ptr cinfo) {
  jpeg_error_mgr * jerr = cinfo->err;

  char message[JMSG_LENGTH_MAX];
  (*cinfo->err->format_message)(cinfo, message);
  //jerr->errorMsg.assign(message);

  throw RuntimeError() << "Jpeg loading error: " << message;
}

/**********************************************************************************************
	DDS Image Class
**********************************************************************************************/
DDSImage::DDSImage(IOFile & data) {
  DDSHeader header;

	_name = data.filename();

  data.Read(header.data, sizeof(header));

  if(header.magic != DDS_MAGIC || header.size != 124 ||
    !(header.flags & DDSD_PIXELFORMAT) || !(header.flags & DDSD_CAPS)) {
    throw RuntimeError() << "Invalid DDS file '" << _name.c_str() << "'.";
  }

  _width = header.width;
  _height = header.height;
  _mipmaps_count = header.mipmaps_count;

  if(PF_IS_DXT1(header.pixel_format) ) {
    _format = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
    _channels = 3;
  } else if( PF_IS_DXT3(header.pixel_format)) {
    _format = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
    _channels = 4;
  } else if( PF_IS_DXT5(header.pixel_format)) {
    _format = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
    _channels = 4;
  /*} else if( PF_IS_BGRA8(header.sPixelFormat)) {
        li = &loadInfoBGRA8;
    } else if( PF_IS_BGR8(header.sPixelFormat)) {
        li = &loadInfoBGR8;
    } else if( PF_IS_BGR5A1(header.sPixelFormat)) {
        li = &loadInfoBGR5A1;
    } else if( PF_IS_BGR565(header.sPixelFormat)) {
        li = &loadInfoBGR565;
    } else if( PF_IS_INDEX8(header.sPixelFormat)) {
        li = &loadInfoIndex8;*/
  } else {

    _width = 0;
    _height = 0;
    _channels = 0;

    throw RuntimeError() << "Unsupported format of DDS file '" << _name.c_str() << "'.";
  }

  _dataSize = data.Size() - sizeof(header);
  _pixels = new GLubyte[_dataSize];
  data.Read(_pixels, _dataSize);

  _loaded = true;

  //fixme: do cube maps later
  //fixme: do 3d later
  /*x = xSize;
    y = ySize;
	glGenTextures(1, texture1);					// Generate OpenGL texture IDs
	glBindTexture(GL_TEXTURE_2D, *texture1);

    mipMapCount = (hdr.dwFlags & DDSD_MIPMAPCOUNT) ? hdr.dwMipMapCount : 1;
    bool hasMipmaps_=false;
    if( mipMapCount > 1 ) {
        hasMipmaps_ = true;
    }
    if( li->compressed ) {
        size_t size = max( li->divSize, x )/li->divSize * max( li->divSize, y )/li->divSize * li->blockBytes;
        unsigned char * data = (unsigned char *)malloc( size );
        if( !data ) {
            return 0;
        }
//    format =  li->internalFormat;
        for( unsigned int ix = 0; ix < mipMapCount; ++ix ) {
            fread( data, 1, size, f );
            glCompressedTexImage2D( GL_TEXTURE_2D, ix, li->internalFormat, x, y, 0, size, data );
            x = (x+1)>>1;
            y = (y+1)>>1;
            size = max( li->divSize, x )/li->divSize * max( li->divSize, y )/li->divSize * li->blockBytes;
        }
        free( data );
    } else if( li->palette ) {
        //  currently, we unpack palette into BGRA
        //  I'm not sure we always get pitch...
        size_t size = hdr.dwPitchOrLinearSize * ySize;
        //  And I'm even less sure we don't get padding on the smaller MIP levels...
    //    format = li->externalFormat;
        //cFormat = li->internalFormat;
        unsigned char * data = (unsigned char *)malloc( size );
        unsigned int palette[ 256 ];
        unsigned int * unpacked = (unsigned int *)malloc( size*sizeof( unsigned int ) );
        fread( palette, 4, 256, f );
        for( unsigned int ix = 0; ix < mipMapCount; ++ix ) {
            fread( data, 1, size, f );
            for( unsigned int zz = 0; zz < size; ++zz ) {
                unpacked[ zz ] = palette[ data[ zz ] ];
            }
            glPixelStorei( GL_UNPACK_ROW_LENGTH, y );
            glTexImage2D( GL_TEXTURE_2D, ix, li->internalFormat, x, y, 0, li->externalFormat, li->type, unpacked );
            x = (x+1)>>1;
            y = (y+1)>>1;
            size = x * y * li->blockBytes;
        }
        free( data );
        free( unpacked );
    } else {
        if( li->swap ) {
            glPixelStorei( GL_UNPACK_SWAP_BYTES, GL_TRUE );
        }
        int size = x * y * li->blockBytes;
        //format = li->externalFormat;
    //    cFormat = li->internalFormat;
        unsigned char * data = (unsigned char *)malloc( size );
        //fixme: how are MIP maps stored for 24-bit if pitch != ySize*3 ?
        for( unsigned int ix = 0; ix < mipMapCount; ++ix ) {
            fread( data, 1, size, f );
            glPixelStorei( GL_UNPACK_ROW_LENGTH, y );
            glTexImage2D( GL_TEXTURE_2D, ix, li->internalFormat, x, y, 0, li->externalFormat, li->type, data );
            x = (x+1)>>1;
            y = (y+1)>>1;
            size = x * y * li->blockBytes;
        }
        free( data );
        glPixelStorei( GL_UNPACK_SWAP_BYTES, GL_FALSE );
    }
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, mipMapCount-1 );
	fclose(f);
    return true;*/
}

DDSImage::~DDSImage() {
  delete [] _pixels;
}

/**********************************************************************************************
	PNG Image Class
**********************************************************************************************/

PNGImage::PNGImage(IOFile & data) {
  png_structp png_ptr;
  png_infop info_ptr;

  int color_type, interlace_type;

	_name = data.filename();

  // png structure
  png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
  if (png_ptr == NULL) {
    throw RuntimeError() << "PNG initialization error.";
  }

  // image info alloc
  info_ptr = png_create_info_struct(png_ptr);
  if (info_ptr == NULL) {
    png_destroy_read_struct(&png_ptr, (png_infopp)NULL, (png_infopp)NULL);
    throw RuntimeError() << "PNG initialization error.";
  }

  // error handling
  //TODO(hacenator): Use png_set_error_fn()
//  if (setjmp(png_jmpbuf(png_ptr))) {
//    png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
//
//    throw RuntimeError() << "PNG file '" << _name.c_str() << "' reading error.";
//  }

  // io control
  png_init_io(png_ptr, data.file());
  png_set_sig_bytes(png_ptr, 0);
  //png_read_info(png_ptr, info_ptr);

  // read
  png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_STRIP_16 | PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND, (png_infopp)NULL);



  _width = png_get_image_width(png_ptr, info_ptr);
  _height = png_get_image_width(png_ptr, info_ptr);
  _channels = png_get_channels(png_ptr, info_ptr);

  switch (png_get_color_type(png_ptr, info_ptr)) {
    case PNG_COLOR_TYPE_RGBA:
      _format = GL_RGBA;
      break;
    case PNG_COLOR_TYPE_RGB:
      _format = GL_RGB;
      break;
    case PNG_COLOR_TYPE_GRAY:
      _format = GL_LUMINANCE;
      break;
    case PNG_COLOR_TYPE_GRAY_ALPHA:
      _format = GL_LUMINANCE_ALPHA;
      break;
    default:
      png_destroy_read_struct(&png_ptr, &info_ptr, NULL);

      _width = 0;
      _height = 0;
      _channels = 0;

      throw RuntimeError() << "Color type of PNG file '" << _name.c_str() << "' not supported.";
  }

  uint32_t row_bytes = png_get_rowbytes(png_ptr, info_ptr);
  _dataSize = row_bytes * _height;
  _pixels = new GLubyte[_dataSize];

  png_bytepp row_pointers = png_get_rows(png_ptr, info_ptr);

  for (int i = 0; i < _height; i++) {
    memcpy(_pixels + (row_bytes * (_height - 1 - i)), row_pointers[i], row_bytes);
  }

  // clean
  png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);

  // sucsesfully loaded
  _loaded = true;
}

PNGImage::~PNGImage() {
  delete [] _pixels;
}

}
